package com.gogant.spider.js;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

public class PhantomProxy {
	private PhantomPool phantomPool;
	private String executeFile = "phantomjs";
	private String proxyFile = "proxy.js";
	private String[] executeOptions = new String[0];
	private Charset charset = Charset.forName("UTF-8");
	private Process process = null;
	private InputStream input = null;
	private OutputStream output = null;
	private MessageReader reader = null;
	private long messageTimeout = 60000;

	protected PhantomProxy(String executeFile, String proxyFile) {
		if (executeFile == null || executeFile.length() == 0 || proxyFile == null || proxyFile.length() == 0) {
			throw new IllegalArgumentException("argus can not be null");
		}
		this.executeFile = executeFile;
		this.proxyFile = proxyFile;
	}

	protected void setPhantomPool(PhantomPool phantomPool) {
		this.phantomPool = phantomPool;
	}

	public String getExecuteFile() {
		return executeFile;
	}

	public String getProxyFile() {
		return proxyFile;
	}

	protected void setExecuteOptions(String[] executeOptions) {
		if (executeOptions == null) {
			executeOptions = new String[0];
		}
		this.executeOptions = executeOptions;
	}

	public List<String> getExecuteOptionList() {
		if (executeOptions == null) {
			return null;
		} else {
			return Arrays.asList(executeOptions);
		}
	}

	public Charset getCharset() {
		return charset;
	}

	public void setCharset(Charset charset) {
		if (charset == null) {
			charset = Charset.forName("UTF-8");
		}
		this.charset = charset;
	}

	public long getMessageTimeout() {
		return messageTimeout;
	}

	public void setMessageTimeout(long messageTimeout) {
		if (messageTimeout <= 0) {
			messageTimeout = 60000;
		}
		this.messageTimeout = messageTimeout;
	}

	public void close() {
		if (phantomPool == null) {
			try {
				doClose();
			} catch (Exception e) {
			}
		} else {
			phantomPool.checkInProxy(this);
		}
	}

	private void initProxy() throws Exception {
		String[] command = new String[executeOptions.length + 2];
		command[0] = executeFile;
		command[command.length - 1] = proxyFile;
		System.arraycopy(executeOptions, 0, command, 0, executeOptions.length);

		try {
			ProcessBuilder pb = new ProcessBuilder(command);
			pb.redirectErrorStream(false);
			process = pb.start();
			input = process.getInputStream();
			output = process.getOutputStream();
			if (reader == null) {
				reader = new MessageReader();
			}
			reader.startup(new BufferedReader(new InputStreamReader(input, charset)));
		} catch (Exception e) {
			doClose();
			throw new Exception("init phantomjs process fail: " + e.getMessage() + ", command: " + Arrays.toString(command));
		}
	}

	private void doClose() {
		if (input != null) {
			try {
				input.close();
			} catch (Exception e) {
			}
			input = null;
		}
		if (output != null) {
			try {
				output.close();
			} catch (Exception e) {
			}
			output = null;
		}
		if (process != null) {
			try {
				process.destroy();
			} catch (Exception e) {
			}
		}
		if (reader != null) {
			reader.shutdown();
		}
	}

	private void fillPageSetting(Command cmd, PageSetting setting) {
		if (setting == null) {
			return;
		}
		cmd.putParam("javascriptEnabled", setting.javascriptEnabled);
		cmd.putParam("loadImages", setting.loadImages);
		cmd.putParam("localToRemoteUrlAccessEnabled", setting.localToRemoteUrlAccessEnabled);
		cmd.putParam("userAgent", setting.userAgent);
		cmd.putParam("userName", setting.userName);
		cmd.putParam("password", setting.password);
		cmd.putParam("XSSAuditingEnabled", setting.XSSAuditingEnabled);
		cmd.putParam("webSecurityEnabled", setting.webSecurityEnabled);
		cmd.putParam("resourceTimeout", setting.resourceTimeout);

	}

	private void fillSizeSetting(Command cmd, Dimension viewSize, Rectangle clipSize) {
		if (viewSize != null) {
			Map<String, Integer> map = new HashMap<String, Integer>();
			map.put("width", viewSize.width);
			map.put("height", viewSize.height);
			cmd.putParam("viewportSize", map);
		}
		if (clipSize != null) {
			Map<String, Integer> map = new HashMap<String, Integer>();
			map.put("left", clipSize.x);
			map.put("top", clipSize.y);
			map.put("width", clipSize.width);
			map.put("height", clipSize.height);
			cmd.putParam("clipRect", map);
		}
	}

	public Page loadPage(URL url, PageSetting setting, String endFunction) throws Exception {
		Command cmd = new Command("loadPage", "url");
		cmd.putParam("url", url.toString());
		cmd.putParam("contentType", "html");
		fillPageSetting(cmd, setting);
		if (endFunction != null) {
			cmd.putParam("endFunction", endFunction);
		}
		Response resp = executeCommand(cmd);
		if (resp.isSuccess()) {
			Page page = new Page();
			page.setCharset(charset);
			page.setContent(resp.getContent());
			page.setURL(url);
			return page;
		} else {
			throw new Exception("can't load page, cause :" + resp.getContent());
		}
	}

	public Page loadPage(URL base, String html, PageSetting setting, String endFunction) throws Exception {
		Command cmd = new Command("loadPage", "url");
		cmd.putParam("url", base.toString()).putParam("text", html);
		cmd.putParam("contentType", "html");
		fillPageSetting(cmd, setting);
		if (endFunction != null) {
			cmd.putParam("endFunction", endFunction);
		}
		Response resp = executeCommand(cmd);
		if (resp.isSuccess()) {
			Page page = new Page();
			page.setCharset(charset);
			page.setContent(resp.getContent());
			page.setURL(base);
			return page;
		} else {
			throw new Exception("can't load page, cause :" + resp.getContent());
		}
	}

	private String getImageFilePath(String path) {
		File file = new File(path);
		try {
			file = file.getCanonicalFile();
		} catch (Exception e) {
		}
		File parent = file.getParentFile();
		if (!parent.exists()) {
			parent.mkdirs();
		}
		return file.getAbsolutePath();
	}

	public File capturePage(URL url, String path, Dimension viewSize, Rectangle clipRect, PageSetting setting, String endFunction) throws Exception {
		Command cmd = new Command("loadPage", "url");
		cmd.putParam("url", url.toString());
		cmd.putParam("contentType", "image");
		cmd.putParam("imagePath", getImageFilePath(path));
		fillPageSetting(cmd, setting);
		fillSizeSetting(cmd, viewSize, clipRect);
		if (endFunction != null) {
			cmd.putParam("endFunction", endFunction);
		}
		Response resp = executeCommand(cmd);
		if (resp.isSuccess()) {
			String content = resp.getContent();
			if (content != null && content.length() > 0) {
				File file = new File(content);
				if (file.exists()) {
					return file;
				}
			}
			throw new Exception("can't capture page, cause: illegal response content: " + content);
		} else {
			throw new Exception("can't capture page, cause: " + resp.getContent());
		}
	}

	public File capturePage(URL base, String html, String path, Dimension viewSize, Rectangle clipRect, PageSetting setting, String endFunction) throws Exception {
		Command cmd = new Command("loadPage", "url");
		cmd.putParam("url", base.toString()).putParam("text", html);
		cmd.putParam("contentType", "image");
		cmd.putParam("imagePath", getImageFilePath(path));
		fillPageSetting(cmd, setting);
		fillSizeSetting(cmd, viewSize, clipRect);
		if (endFunction != null) {
			cmd.putParam("endFunction", endFunction);
		}
		Response resp = executeCommand(cmd);
		if (resp.isSuccess()) {
			String content = resp.getContent();
			if (content != null && content.length() > 0) {
				File file = new File(content);
				if (file.exists()) {
					return file;
				}
			}
			throw new Exception("can't capture page, cause: illegal response content: " + content);
		} else {
			throw new Exception("can't capture page, cause: " + resp.getContent());
		}
	}

	public void enableCookie(boolean enable) throws Exception {
		Command cmd = new Command("modCookie", "enable");
		cmd.putParam("cookieEnable", enable);
		Response resp = executeCommand(cmd);
		if (!resp.isSuccess()) {
			throw new Exception("can't enable cookie: " + enable);
		}
	}

	public boolean isEnableCookie() throws Exception {
		Command cmd = new Command("modCookie", "enable");
		Response resp = executeCommand(cmd);
		if (!resp.isSuccess()) {
			throw new Exception("can't get cookie status");
		}
		return Boolean.valueOf("" + resp.getParam("cookieEnable"));
	}

	public void addCookie(Cookie cookie) throws Exception {
		Command cmd = new Command("modCookie", "add");
		cmd.putParam("cookie", cookie);
		Response resp = executeCommand(cmd);
		if (!resp.isSuccess()) {
			throw new Exception("can't add cookie: " + cookie);
		}
	}

	public void clearCookies() throws Exception {
		Command cmd = new Command("modCookie", "clear");
		Response resp = executeCommand(cmd);
		if (!resp.isSuccess()) {
			throw new Exception("can't clear cookie");
		}
	}

	public boolean deleteCookie(String name) throws Exception {
		Command cmd = new Command("modCookie", "delete");
		cmd.putParam("cookieName", name);
		Response resp = executeCommand(cmd);
		if (!resp.isSuccess()) {
			throw new Exception("can't delete cookie: " + name);
		}
		return true;
	}

	public List<Cookie> getCookieList() throws Exception {
		Command cmd = new Command("modCookie", "get");
		Response resp = executeCommand(cmd);
		if (!resp.isSuccess()) {
			throw new Exception("can't get cookie list");
		}
		List<Cookie> list = new ArrayList<Cookie>();
		Object values = resp.getParam("cookies");
		if (values == null || !(values instanceof JSONArray)) {
			return list;
		}
		JSONArray array = (JSONArray) values;
		for (int i = 0; i < array.size(); i++) {
			JSONObject obj = array.getJSONObject(i);
			Cookie cookie = new Cookie(obj.getString("name"), obj.getString("value"), obj.getString("domain"));
			list.add(cookie);
		}
		return list;
	}

	private synchronized Response executeCommand(Command cmd) throws Exception {
		String jsonMsg = cmd.toJSONString();
		if (output == null) {
			initProxy();
		}
		if (!writeMessage(jsonMsg)) {
			initProxy();
			if (!writeMessage(jsonMsg)) {
				throw new Exception("can't send message to phantomjs process");
			}
		}
		String message = readMessage(messageTimeout);
		try {
			return new Response(message);
		} catch (Exception e) {
			throw new Exception("can't parse message: " + e.getMessage());
		}
	}

	private boolean writeMessage(String msg) {
		try {
			int length = input.available();
			if (length > 0) {
				input.skip(length);
			}
			output.write(msg.getBytes(charset));
			output.write('\n');
			output.flush();
			return true;
		} catch (Throwable e) {
			doClose();
			return false;
		}
	}

	private String readMessage(long timeout) throws Exception {
		String message = reader.readLine(timeout);
		if (message == null) {
			throw new Exception("can't read message from phantomjs process");
		} else {
			return message;
		}
	}

	private class Command {
		private String method;
		private String type;
		private Map<String, Object> paramMap = new HashMap<String, Object>();

		public Command(String method, String type) {
			this.method = method;
			this.type = type;
		}

		public Command putParam(String name, Object value) {
			paramMap.put(name, value);
			return this;
		}

		public String toJSONString() {
			JSONObject obj = new JSONObject(paramMap);
			obj.put("method", method);
			obj.put("type", type);
			obj.put("encoding", charset.name());
			return obj.toJSONString();
		}
	}

	private class Response {
		private boolean result;
		private String content;
		private Map<String, Object> paramMap = new HashMap<String, Object>();

		public Response(String message) {
			JSONObject obj = JSON.parseObject(message);
			result = obj.getBooleanValue("result");
			content = obj.getString("content");
			obj.remove("result");
			obj.remove("content");
			for (String key : obj.keySet()) {
				paramMap.put(key, obj.get(key));
			}
		}

		public boolean isSuccess() {
			return result;
		}

		public String getContent() {
			return content;
		}

		public Object getParam(String key) {
			return paramMap.get(key);
		}

	}

	private class MessageReader implements Runnable {
		private Object readLock = new Object();
		private boolean isWaiting = false;
		private String message;
		private BufferedReader reader;
		private Thread readerThread;

		public void run() {
			while (true) {
				synchronized (readLock) {
					if (!isWaiting) {
						try {
							readLock.wait();
						} catch (InterruptedException e) {
						}
						continue;
					}
					try {
						message = null;
						BufferedReader tempReader = null;
						synchronized (this) {
							if (reader == null) {
								return;
							} else {
								tempReader = reader;
							}
						}
						boolean isLineStart = false;
						String line = null;
						while ((line = tempReader.readLine()) != null) {
							if (isLineStart) {
								break;
							} else {
								isLineStart = "<--response start-->".equals(line);
							}
						}
						if (line == null || !isLineStart) {
							doClose();
							continue;
						}
						message = line;
					} catch (Throwable e) {
					} finally {
						isWaiting = false;
						readLock.notifyAll();
					}
				}
			}
		}

		public synchronized void startup(BufferedReader reader) {
			if (reader == null) {
				return;
			}
			this.reader = reader;
			readerThread = new Thread(this, "MessageReaderThread");
			readerThread.setDaemon(true);
			readerThread.start();
		}

		public synchronized void shutdown() {
			reader = null;
			if (readerThread != null) {
				readerThread.interrupt();
			}
		}

		private long waitFor(long timeout) {
			if (timeout <= 0) {
				timeout = 60000;
			}
			synchronized (readLock) {
				long start = System.currentTimeMillis();
				try {
					readLock.wait(timeout);
					return timeout + System.currentTimeMillis() - start;
				} catch (InterruptedException e) {
					return -1;
				}
			}
		}

		public String readLine(long timeout) throws Exception {
			if (reader == null) {
				return null;
			}
			synchronized (readLock) {
				isWaiting = true;
				message = null;
				readLock.notify();
				timeout = waitFor(timeout);
				isWaiting = false;
				if (timeout <= 0) {
					throw new Exception("read message timeout");
				} else {
					return message;
				}
			}
		}
	}

	// private class ErrorReader implements Runnable {

	public static void main(String[] args) throws Exception {
		PageSetting setting = new PageSetting();
//		setting.javascriptEnabled = true;
		setting.loadImages = true;
		PhantomProxy proxy = new PhantomProxy("phantomjs/windows/phantomjs", "phantomjs/proxy.js");
		File file = proxy.capturePage(new URL("http://www.taobao.com"), "./data/taobao.png", null, null, setting, null);
		System.out.println(file);
		Thread.sleep(10000);
	}
}
